#-*- coding:utf-8 -*-

# The format of the comments: (Using "//" below to denote the comment string of the current file. Different kinds of comment string can be  defined in the config file. )
# 1. "//#define xx yy " define xx = yy
# 2. "//#if[n]def xx [...]" or "//#platform xx [...]" at the tail of a line, control this line. 
# 3. "//##if[n]def xx [...]" or "//##platform xx [...]" continue to control until the corresponding "//##endif" or "//##endplatform"
# 4. Defined keys enclosed by `` will be replaced by the defined value. (`DEBUG` will be replace by 1 if defined DEBUG to 1)

# Note: [XXX] means XXX can be present or not. 

import os
import sys
import json
import copy
import re
import shutil

projectDir = os.getcwd()
config =  {
    "deleteComment": {
        "c": 0, 
        "classpath": 0, 
        "cpp": 0, 
        "h": 0, 
        "java": 0, 
        "project": 0, 
        "xml": 0
    }, 
    "exclude": {
        "All": [
          "java_repackage_ports/.*", 
          "gen/.*", 
          "xtend-gen/.*"
        ],
    }, 
    "extract": {
        "All": [
            "src/.*", 
            "libs/.*", 
            "assets/.*", 
            "jni/.*", 
            "xtend-gen/.*",
            "\\.classpath", 
            "\\.project"
        ], 
        "Android": [
            "res/.*", 
            "androidmanifest\\.xml", 
            "proguard\\.cfg", 
            "project\\.properties"
        ]
    }, 
    "lineCommentBeginString": {
        "c": "//", 
        "classpath": "<!--", 
        "cpp": "//", 
        "h": "//", 
        "java": "//", 
        "project": "<!--", 
        "xml": "<!--"
    }, 
    "platformDeduced": {
        "Linux": [
            "Java"
        ], 
        "Windows": [
            "Java"
        ]
    }, 
    "process": [
        ".*\\.java", 
        "\\.classpath", 
        "\\.project"
    ]
}

platforms = []
process=[] # The relative paths in regx to be preprocessed
predefined= {} # All the key-val pairs which are defined in the config.repackage file 
defined = {} # All the key-val pairs defined for now. These pairs come from config.repackage and the file content. The initial value of this list is reset to predefined before the processing of each new file
extract=[] # The relative paths in regx to be extracted
exclude = [] # The relative paths in regx to be excluded from the extraction. And the list {extract}-{exclude} is the final list to be extracted. 

handled = False
word = re.compile("[a-zA-Z_][a-zA-Z_0-9]*")
nonspace = re.compile(r"[^\s]+")
defWord = re.compile(r"`[a-zA-Z_][a-zA-Z_0-9]*`")

def addRecursively(value, newValue):
  if type(value) == type(newValue):
    if isinstance(value, dict):
      for k in newValue.keys():
        if k in value.keys():
          addRecursively(value[k], newValue[k])
        else:
          value[k] = newValue[k]
    elif isinstance(value, (list, set)):
      for val in newValue:
        if isinstance(val, dict):
          if val in value.keys():
            addRecursively(value[val], newValue[val])
          else:
            value.append(val)
        elif not val in value:
          value.append(val)
    else:
      value = newValue
  else:
    print "Type mismatch!! ", value, type(value), newValue, type(newValue)
    return False

def readConfig():
  global config

  if os.path.exists("config.repackage") and os.path.isfile("config.repackage"):
      h = open("config.repackage", "r")
  else:
    return False

  newConfig = json.load(h)
  addRecursively(config, newConfig)
  h.close()

  return True

def updateConfig(thisPlatform):
  global config
  global platforms
  global predefined
  global defined
  global process
  global extract
  global exclude

  #try:
    #while True:
      #platforms.pop()
  #except:
    #pass

  #try:
    #while True:
      #process.pop()
  #except:
    #pass

  #try:
    #while True:
      #extract.pop()
  #except:
    #pass

  #try:
    #while True:
      #exclude.pop()
  #except:
    #pass
  platforms = []
  process=[]
  extract=[]
  exclude=[]
  predefined={}
  defined.clear()

  platform = thisPlatform

  knownPlatforms = []

  platforms = [platform]
  if config["platformDeduced"].has_key(platform):
    for p in config["platformDeduced"][platform]:
      platforms.append(p)

  knownPlatforms += platforms

  for p in config["platformDeduced"].keys()+config["exclude"].keys()+config["extract"].keys():
    knownPlatforms.append(p)
  for key in config["platformDeduced"].keys():
    for p in config["platformDeduced"][key]:
      knownPlatforms.append(p)
  knownPlatforms = set(knownPlatforms)

  predefined = copy.deepcopy(config["defined"])
  predefined["platform"] = platform
  for p in platforms:
    if not predefined.has_key(p):
      predefined[p] = ""

  defined = copy.deepcopy(predefined)
  process = iniFileList(config["process"])

  for p in platforms:
    try:
      exclude += iniFileList(config["exclude"][p])
    except:
      print "WANING: exclude of platform", p, "not defined"
  for p in platforms:
    try:
      extract += iniFileList(config["extract"][p])
    except:
      print "WANING: extract of platform", p, "not defined"
  try:
    exclude += iniFileList(config["exclude"]["All"])
  except:
    print "WANING: exclude of All not defined"
  try:
    extract += iniFileList(config["extract"]["All"])
  except:
    print "WANING: extract of All not defined"

  for p in knownPlatforms:
    if not p in platforms:
      try:
        p = p.lower()
      except:
        pass

      exclude.append(toReg(".*port_"+p+".*"))   # To exclude file from other platforms at a high level of intelligence, one file should be excluded as long as the file path contains the name of some other platform and with a prefix "port_" 

  return True

def iniFileList(list):
  l = []
  for i in list:
    s = expandFileName(i)
    re = toReg(s)
    l.append(re)
  return l

def toReg(i):
  return re.compile(i, re.I)

def dumpJson(path):
  h = open(path, "w")
  json.dump(config, h ,sort_keys=True, indent=2)
  h.close()

def iniConfigFile():
  dumpJson("config.repackage")

def expandFileName(name):
  name = trimFileName(projectDir)+ "/" + name.lower()
  return name

def trimFileName(file):
  s = file.lower()
  s = file.replace("\\", "/")
  return s

def trimFileNames(list):
  l = []
  for i in list:
    l.append(trimFileName(i))
  return l

def processFile(fileName):
  global config
  global platforms
  global predefined
  global defined
  global process
  global extract
  global exclude
  global handled

  def checkBlock(lineparts, blockValid, wd):
    global handled
    if lineparts[1].startswith(wd):
      token = word.findall(lineparts[1][len(wd) + 1:])[0]
      if wd == "platform":
        blockValid.append(token in platforms)
      elif wd == "ifndef":
        blockValid.append(not token in defined.keys())
      else:
        blockValid.append(token in defined.keys())

      handled |= True
    else:
      handled |= False

  def checkLine(lineparts, wd):
    global handled
    if lineparts[1].startswith(wd):
      token = word.findall(lineparts[1][len(wd) + 1:])[0]
      handled = True
      if wd == "platform":
        return token in platforms
      elif wd == "ifndef":
        return not token in defined.keys()
      else:
        return token in defined.keys()
    else:
      return False

  def info(lnum, fileName, inf):
    print "info in", fileName, "line", lnum, inf

  def addDef(inst, defined, lnum, fileName):
    tmp = nonspace.findall(inst)
    if len(tmp) >= 2:
      if defined.has_key(tmp[1]):
        info (lnum, fileName, "redefine: " +  tmp[1])
    try:
      defined[tmp[1]] = tmp[2]
    except:
      try:
        defined[tmp[1]] = ""
      except:
        info(lnum, fileName, "define xx yy")

  def addDefs(lineparts, defined, lnum, fileName):
    if "[" in lineparts[1] and "]" in lineparts[1]:
      instructions = lineparts[1][lineparts[1].find("[") + 1: lineparts[1].find("]")]
      for inst in instructions.split(","):
        # handle define
        if "define" in inst:
          addDef(inst, defined, lnum, fileName)

  defined = copy.deepcopy(predefined)
  h = open(fileName.decode('utf-8').encode('gbk'), "r")
  cms = config["lineCommentBeginString"][getExt(fileName)]
  deleteComment = config["deleteComment"][getExt(fileName)]
  lines = h.readlines()
  trimedLines = []
  blockValid = []
  lnum = 0
  for line in lines:
    lnum += 1
    handled = False
    defValid = False # Being True indicates the validation of the definition

    if (cms+"\\\\") in line:  #A line starts with a "//\\" should be regarded as a normal line without this header. 
      line = line.replace(cms+"\\\\", "",1)

    if blockValid:
      #blockValid isn't empty, we are in a block
      if cms + "##else" in line:
        blockValid[-1] = not blockValid[-1]
      elif cms + "##end" in line:
        if blockValid[-1]:
          if deleteComment:
            trimedLines.append(line.split(cms + "##end")[0])
          else:
            trimedLines.append(line)
        blockValid.pop()
        continue # Skip this line
      elif not blockValid[-1]:
        continue

    #The processing logic of a single line is the same no matter in a block or not. 
    lineparts = line.split(cms)
    line = lineparts[0]
    defParts = line.split("`")
    if len(defParts) % 2 == 0:
      info(lnum, fileName, "define not closed")
    else:
      i = 0
      for i in range(len(defParts) / 2):
        try:
          defParts[2 * i + 1] = defined[defParts[2 * i + 1]]
        except:
          defParts[2 * i + 1] = "`" + defParts[2 * i + 1] + "`"
    line = "".join(defParts)
    lineparts[0] = line
    line = cms.join(lineparts)

    if cms + "##" in line:      # beginning of the block control
      lineparts = line.split(cms + "##")
      checkBlock(lineparts, blockValid, "ifdef")
      checkBlock(lineparts, blockValid, "ifndef")
      checkBlock(lineparts, blockValid, "platform")
      defValid = blockValid[-1]
    elif cms + "#define" in line:
      lineparts = line.split(cms + "#")
      # control a single line
      addDef(lineparts[1], defined, lnum, fileName)
    elif cms + "#" in line:
      lineparts = line.split(cms + "#")
      defValid |= checkLine(lineparts, "ifdef")
      defValid |= checkLine(lineparts, "ifndef")
      defValid |= checkLine(lineparts, "platform")
    else:
      lineparts = [line]

    if handled and defValid:
      addDefs(lineparts, defined, lnum, fileName)

    if not handled or defValid:
      if deleteComment:
        trimedLines.append(lineparts[0])
      else:
        trimedLines.append(line)

  return trimedLines

def getExt(fileName):
  s = ""
  try:
    s = fileName.split(".")[-1]
  except:
    s = ""
  return s

def work(root, exroot):
  root = trimFileName(root)
  exroot = trimFileName(exroot)

  if os.path.exists(exroot):
    filenames=os.listdir(exroot)
    if filenames:
      if raw_input(exroot + " not empty! go ahead anyway?[y/n]") != "y":
        return
      else:
        for f in filenames:
          f = (exroot + "/" + f).encode('gbk')
          if os.path.isfile(f):
            os.remove(f)
            print f+" removed!"
          elif os.path.isdir(f):
            shutil.rmtree(f,True)
            print "dir "+f+" removed!"

  for dirpath, dirnames, filenames in os.walk(root):
    dirpath = trimFileName(dirpath).decode('gbk').encode('utf-8')

    exDir = dirpath.replace(root, exroot)
    for filename in filenames:
      filename = filename.decode('gbk').encode('utf-8')
      filename = dirpath + "/" + filename
      exFile = filename.replace(root, exroot)

      excluded = False      
      for exc in exclude:
        if exc.match(filename):
          excluded = True
          print "file", filename, "ignored"
      if excluded:
        continue

      processed = False
      for m in process:
        if m.match(filename):
          if not os.path.exists(exDir):
            os.makedirs(exDir)
          h = open(exFile, "w")
          h.writelines(processFile(filename))
          h.close()
          processed = True
          print "processed", filename
          break
      if processed:
        continue

      for ex in extract:
        if ex.match(filename):
          if not os.path.exists(exDir):
            os.makedirs(exDir)
          shutil.copyfile(filename, filename.replace(root, exroot))
          print "extracted", filename
          break

if __name__ == "__main__":
  default_encoding = 'utf-8'
  if sys.getdefaultencoding() != default_encoding:
    reload(sys)
    sys.setdefaultencoding(default_encoding)

  if readConfig():
    pass
  else:
    con = raw_input (u"The configuration file [config.repackage] doesn't exits, continue with the default configuration? [y/n]")
    if not con.startswith("y"):
      iniConfigFile()
      print u"Cancelled. Please edit config.repackage first"
      exit()

  print u"***********Current configurations are dumped to the file [config.dump]"
  dumpJson("config.dump")

  for p in config["platformInTurn"]:
    updateConfig(p)
    work(os.getcwd(), os.getcwd() + "/java_repackage_ports/" + p)
  print "done"
